-
-
Notifications
You must be signed in to change notification settings - Fork 198
Email users code #2449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Email users code #2449
Conversation
48dc0a3 to
5ac09e3
Compare
9fde866 to
8c8f601
Compare
|
Alright, code mostly written. Now just need to figure out how to set it to be run on a regular basis |
This starts us down the path of using Active Job, which is the preferred mechanism for jobs in Rails 7.1. Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
I've taken the copy from Kimberly and put it into HTML, as we do with the rest of our emails Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
This adds an additional method to the MemberMailer class. It's currently called #chaser and it sends...the chaser email. In the next commit I'll call it from a Job, or maybe a Service that's called from a Job Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
This is the setup required for using ActiveJob and its queue, and checking if things were enqueued. Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
This was an oversight when creating the related table, and fixes it Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
This outlines a single test for sending the chaser email. It also creates the relevant Job and Service. When users haven't attended a workshop they were invited to in the last three months _and_ haven't already been chased, they'll be sent a follow up email Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
To note - the first time we run this, there's likely to be a massive spike in emails. To set this in Heroku, we'll need to log into the interface and call it as `rake chaser:three_months` Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com>
8c8f601 to
a049c41
Compare
octoberclub
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great so far. Got it to work with some tweaks. Have also made some suggestions.
Feel free to bounce any comments back to me. Happy to fork and do another PR to your branch myself if that's easier?
Runthrough of what I checked
- Browsed through the code, and compared with my own solution (in a local branch)
- Used AI with superpowers plugin to check the changes and compare
- Did some testing manually
** AI prompt **
Can you check through the changes to see if any issues come up? Specifically, I wondered if the query in the send_chaser method needed to also check subscriptions in case it accidentally includes unsubscribed users? Anything else to note?
Response:
Findings
High: The send_chaser query does not check active subscriptions or student role, so it can include members who have unsubscribed from all groups or are coaches. If the intent is “students who haven’t attended in 3 months,” it should scope to student subscriptions (e.g., joins(:groups).merge(Group.students) or joins(:subscriptions) plus role filtering). File: [three_month_email_service.rb](https://file+.vscode-resource.vscode-cdn.net/Users/michellesteele/.vscode/extensions/openai.chatgpt-0.4.71-darwin-arm64/webview/#).
High: The query logic is based on workshop_invitations.created_at and attended: false, which doesn’t actually mean “has not attended in 3 months.” It will include members who have attended recently but also have a non-attended invitation in the last 3 months. It should look for members with no attended workshops since a cutoff, using workshops.date_and_time and WorkshopInvitation.attended scoped to students. File: [three_month_email_service.rb](https://file+.vscode-resource.vscode-cdn.net/Users/michellesteele/.vscode/extensions/openai.chatgpt-0.4.71-darwin-arm64/webview/#).
Medium: The email template HAML is mis-indented, so the %p tags render empty and the text is output as plain text lines. You likely want the lines indented under each %p. File: [three_month_chaser.html.haml](https://file+.vscode-resource.vscode-cdn.net/Users/michellesteele/.vscode/extensions/openai.chatgpt-0.4.71-darwin-arm64/webview/#).
Low: [members.each](https://file+.vscode-resource.vscode-cdn.net/Users/michellesteele/.vscode/extensions/openai.chatgpt-0.4.71-darwin-arm64/webview/#) in send_chaser could be find_each to avoid loading a large set into memory. File: [three_month_email_service.rb](https://file+.vscode-resource.vscode-cdn.net/Users/michellesteele/.vscode/extensions/openai.chatgpt-0.4.71-darwin-arm64/webview/#).
Direct answer to your question
Yes. As written, send_chaser can include unsubscribed users because it never checks subscriptions or groups. If you only want active student subscribers, it needs that scope.
If you want, I can propose a corrected query that matches the “no attendance in 3 months” requirement and respects subscriptions.
Testing:
I tested locally using this in the config which output the emails as separate files into tmp/mails folder in the container.
config.action_mailer.delivery_method = :file
config.action_mailer.file_settings = { location: Rails.root.join('tmp', 'mails') }
I created some members who had not attended in three months via rails console
docker compose exec web bundle exec rails c
member = Fabricate(:member, auth_services: [
Fabricate(:auth_service, uid: SecureRandom.hex(8))
])
chapter = Fabricate(:chapter)
group = Fabricate(:group, name: 'Students', chapter: chapter)
Fabricate(:subscription, member: member, group: group)
old_workshop = Fabricate(:workshop, chapter: chapter, date_and_time: 4.months.ago)
Fabricate(:workshop_invitation, member: member, workshop: old_workshop, attended: false, role: 'Student')
Then I ran the service
ThreeMonthEmailService.send_chaser
Tweaks needed for testing manually:
I had to change send_chaser a bit to get it to work (might be to do with my setup though)
- Update the query to check for attended: nil instead of false
- Change to
deliver_now
Then you can find the email output in tmp/mails
|
|
||
| private | ||
|
|
||
| def log_sent_email |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea to put it in a shared concern for future.
| members = Member.joins(:workshop_invitations) | ||
| .left_joins(:member_email_deliveries) | ||
| .where(workshop_invitations: { attended: false }) | ||
| .where("workshop_invitations.created_at >= ?", 3.months.ago.beginning_of_day) | ||
| .where(member_email_deliveries: { id: nil }) | ||
| .distinct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There might be a bit more to the query as it has to exclude certain members
- unsubscribed members
- possibly coaches(?) One to check with Kimberley?
- banned members
- not accepted terms and conditions
Also the logic here only checks created_at>=3 months and attended:false which would still include members who did attend in the last three months but also have a non-attended workshop in that period
| members = Member.joins(:workshop_invitations) | |
| .left_joins(:member_email_deliveries) | |
| .where(workshop_invitations: { attended: false }) | |
| .where("workshop_invitations.created_at >= ?", 3.months.ago.beginning_of_day) | |
| .where(member_email_deliveries: { id: nil }) | |
| .distinct | |
| recent_attendees = Member.joins(:workshop_invitations) | |
| .merge( | |
| WorkshopInvitation.attended.to_students | |
| .joins(:workshop) | |
| .where('workshops.date_and_time >= ?', 3.months.ago.beginning_of_day) | |
| ) | |
| .distinct | |
| members = Member.not_banned | |
| .accepted_toc | |
| .joins(:groups) | |
| .merge(Group.students) | |
| .left_joins(:member_email_deliveries) | |
| .where(member_email_deliveries: { id: nil }) | |
| .where.not(id: recent_attendees.select(:id)) | |
| .distinct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I tested (with fake data), the attended column was nil rather than false which also failed the query. I'm not sure if that's the same in our live db
| .where(member_email_deliveries: { id: nil }) | ||
| .distinct | ||
| return if members.empty? | ||
| members.each do |member| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use find_each to lower the amount of records read into memory in one go
| members.each do |member| | |
| members.find_each do |member| |
| %h1 Hi #{@member.name}, | ||
|
|
||
| %p | ||
| We’ve noticed you haven’t been to a codebar workshop in a little while, and we just wanted to check in. We know life gets busy, but we’d love to understand how things are going for you and whether there’s anything we can do to make it easier or more valuable for you to join again. | ||
| %p | ||
| If you have a minute, could you please share your thoughts in this short form? 👉 https://forms.gle/tEETvC3zYP9mcLar7 | ||
|
|
||
| %p | ||
| Or, if you’re thinking about coming back soon, we’ve got some great upcoming workshops and events you might like to join 👉https://codebar.io/events/ | ||
|
|
||
| %p | ||
| Your feedback really helps us make codebar more welcoming and useful for everyone in our community 💜 | ||
|
|
||
| %p | ||
| We’d love to see you again soon! | ||
|
|
||
| %p | ||
| #{"-- "} | ||
| %br | ||
| Warmly, | ||
| The Codebar Team |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks as if the multiline text under each %p section needs indenting for it to display. At least this is how .slim files work and I'm assuming .haml works the same.
As an aside, would be better to move everything to .html.erb eventually? Much better format, although it's beyond this issue, might be worth discussing in future?
| %h1 Hi #{@member.name}, | |
| %p | |
| We’ve noticed you haven’t been to a codebar workshop in a little while, and we just wanted to check in. We know life gets busy, but we’d love to understand how things are going for you and whether there’s anything we can do to make it easier or more valuable for you to join again. | |
| %p | |
| If you have a minute, could you please share your thoughts in this short form? 👉 https://forms.gle/tEETvC3zYP9mcLar7 | |
| %p | |
| Or, if you’re thinking about coming back soon, we’ve got some great upcoming workshops and events you might like to join 👉https://codebar.io/events/ | |
| %p | |
| Your feedback really helps us make codebar more welcoming and useful for everyone in our community 💜 | |
| %p | |
| We’d love to see you again soon! | |
| %p | |
| #{"-- "} | |
| %br | |
| Warmly, | |
| The Codebar Team | |
| %h1 Hi #{@member.name}, | |
| %p | |
| We’ve noticed you haven’t been to a codebar workshop in a little while, and we just wanted to check in. We know life gets busy, but we’d love to understand how things are going for you and whether there’s anything we can do to make it easier or more valuable for you to join again. | |
| %p | |
| If you have a minute, could you please share your thoughts in this short form? 👉 https://forms.gle/tEETvC3zYP9mcLar7 | |
| %p | |
| Or, if you’re thinking about coming back soon, we’ve got some great upcoming workshops and events you might like to join 👉https://codebar.io/events/ | |
| %p | |
| Your feedback really helps us make codebar more welcoming and useful for everyone in our community 💜 | |
| %p | |
| We’d love to see you again soon! | |
| %p | |
| #{"-- "} | |
| %br | |
| Warmly, | |
| The Codebar Team |
| end | ||
| end | ||
|
|
||
| describe "#chaser" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice spec.
| @@ -0,0 +1,43 @@ | |||
| RSpec.describe ThreeMonthEmailService, type: :service do | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could add a few scenarios around different types of attendance - like people who had not attended one workshop but then attended the next one in a previous 3 months.
Also check we don't send out for banned, or unsubscribed members.
| include EmailHeaderHelper | ||
| include EmailDelivery | ||
|
|
||
| after_action :log_sent_email, only: [:chaser] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to log only once the email is sent
| after_action :log_sent_email, only: [:chaser] | |
| after_deliver :log_sent_mail , only: [:chaser] |
This code should close #2383 , and form a decent basis for other tasks in that group.
It collates the users who've not accepted a workshop invite in the last three months and who haven't already been chased, and sends them a chaser email.